<?php
namespace App\Libraries\NoSql\Mongo;

use Phalcon\Acl\Exception;

/**
 * MongoDB 客户端类 php_mongo 扩展实现
 * @package App\Libraries\NoSql\Mongo
 */
class MongodbClient {

	#region 属性

	/**
	 * MongoClient 对象
	 * @var \MongoClient
	 */
	private $client;

	/**
	 * Mongdodb 数据库名称
	 * @var string
	 */
	private $dbName;

	/**
	 * MongoClient 对象
	 * @var \MongoDB
	 */
	private $db;

	/**
	 * Mongdodb配置信息
	 * @var \Phalcon\Config
	 */
	private $config;

	/**
	 * 要返回的字段
	 * @var array
	 */
	private $fields = array();

	/**
	 * 设置要返回的字段
	 * @param  array $fields 查询数据要返回的字段
	 * @return $this
	 */
	public function fields(array $fields) {
		if ($fields)
			$this->fields = $fields;
		return $this;
	}

	/**
	 * 查询数据的查询条件
	 * @var array
	 */
	private $where = array();

	/**
	 * 设置查询数据的查询条件
	 * @param array $where 查询数据的查询条件
	 * @return $this
	 */
	public function where(array $where) {
		$this->where = array_merge($where, $this->where);
		return $this;
	}

	/**
	 * 排序字段 1位升序，-1降序
	 * @var array
	 */
	private $sort = array();

	/**
	 * 排序字段
	 * @param array $sort 排序字段  1位升序，-1降序
	 * @return $this
	 */
	public function sort(array $sort) {
		$this->sort = $sort;
		return $this;
	}

	/**
	 * 限制 设置返回结合中匹配搜索条件的前N个数据项
	 * @var array
	 */
	private $limit = 0;

	/**
	 * 设置查询数据的查询条件
	 * @param $limit 查询数据的查询条件
	 * @return $this
	 */
	public function limit($limit) {
		$this->limit = intval($limit);
		return $this;
	}

	/**
	 * 忽略数 忽略前n个匹配搜索条件的结构
	 * @var array
	 */
	private $skip = 0;

	/**
	 * 设置忽略数
	 * @param $skip 忽略数
	 * @return $this
	 */
	public function skip($skip) {
		$this->skip = intval($skip);
		return $this;
	}

	/**
	 * 指定查询数据时使用的索引
	 * @var array
	 */
	private $hint = array();

	/**
	 * 指定查询数据时使用的索引
	 * @param array $hint 查询数据时使用的索引
	 * @return $this
	 */
	public function hint(array $hint) {
		$this->hint = $hint;
		return $this;
	}

	/**
	 * 需要更新的数据
	 * @var array
	 */
	private $update_data = array();

	#endregion

	#region 构造函数

	/**
	 * 构造函数
	 * @param $config mongodb 配置
	 */
	public function __construct($config) {
		$this->config = $config;
		$this->dbName = $this->config->options->db;
	}

	#endregion

	#region 插入数据

	/**
	 * 插入数据
	 * @param $collectionName 集合名称
	 * @param array $data 要插入的数据
	 * @param array $options 插入数据的参数
	 * @return bool
	 */
	public function insert($collectionName, array $data, array $options = array("fsync" => FALSE, "j" => 1, "w" => 1)) {
		//初始化
		$this->_init();
		$collection = $this->_selectCollection($collectionName);
		if ($collection) {
			return $collection->insert($data, $options);
		}
		return FALSE;
	}

	/**
	 * 批量插入数据
	 * @param $collectionName 集合名称
	 * @param array $data 要批量插入的数据
	 * @param array $options 插入数据的参数
	 * @return bool
	 */
	public function batchInsert($collectionName, array $data, array $options = array("fsync" => FALSE, "j" => 1, "w" => 1)) {
		//初始化
		$this->_init();
		$collection = $this->_selectCollection($collectionName);
		if ($collection) {
			return $collection->batchInsert($data, $options);
		}
		return FALSE;
	}

	#endregion

	#region 更新数据

	/**
	 * 更新/插入数据
	 * @param $collectionName 集合名称
	 * @param array $data 更新匹配记录的对象
	 * @param array $options 更新的相关参数
	 * @return array | boolean
	 * @throws /Exception
	 */
	public function save($collectionName, array $data, array $options = array("multiple" => true, "fsync" => FALSE, "w" => 1, "j" => FALSE)) {
		if (empty($data)) {
			throw new Exception("没有设置要更新的对象");
		}
		//初始化
		$this->_init();
		$collection = $this->_selectCollection($collectionName);
		if ($collection) {
			return $collection->save($data, $options);
		}
		return FALSE;
	}

	/**
	 * 更新数据
	 * @param $collectionName 集合名称
	 * @param array $data 更新匹配记录的对象
	 * @param array $options 更新的相关参数
	 * @return array | boolean
	 * @throws /Exception
	 */
	public function update($collectionName, array $data = array(), array $options = array("multiple" => true, "upsert" => true, "fsync" => FALSE, "w" => 1, "j" => FALSE)) {
		if (empty($data) && empty($this->update_data)) {
			throw new Exception("没有设置要更新的对象");;
		}
		if (!empty($data)) {
			$data = array_merge($this->update_data, $data);
		} else {
			$data = $this->update_data;
		}
		$this->update_data = array();
		//初始化
		$this->_init();
		$collection = $this->_selectCollection($collectionName);
		if ($collection) {
			$where = $this->where;
			$this->where = array();
			return $collection->update($where, $data, $options);
		}
		return FALSE;
	}

	/**
	 * 以原子的方式修改文档
	 * @param $collectionName 集合名称
	 * @param array|NULL $data 用于更新文档的信息
	 * @param array|NULL $options 指定选项
	 * @return array
	 * @throws /Exception
	 */
	public function findAndModify($collectionName, array $data = NULL,
								  array $options = array("sort" => null, "remove" => FALSE, "new" => true, "upsert" => true)) {

		if (empty($data) && empty($this->update_data)) {
			throw new Exception("没有设置要更新的对象");
		}
		if (!empty($data)) {
			$data = array_merge($this->update_data, $data);
		} else {
			$data = $this->update_data;
		}
		$this->update_data = array();
		//初始化
		$this->_init();
		$collection = $this->_selectCollection($collectionName);
		if ($collection) {
			$where = $this->where;
			$this->where = array();
			//"sort" => null, "remove" => FALSE, "update" => true, "new" => true, "upsert" => true
			return $collection->findAndModify($where, $data, $this->fields, $options);
		}
		return FALSE;
	}

	/**
	 * 更新数据 - 使用$ince增加特定键的值
	 * @param array $data 要增加特定键的值
	 *                  $data=array("fieldName"=>increaseValue);
	 * @return $this
	 */
	public function ince(array $data) {
		if (!key_exists('$inc', $this->update_data))
			$this->update_data['$inc'] = $data;
		else {
			$this->update_data['$inc'] = array_merge($this->update_data['$inc'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$set 修改键值
	 * @param array $data 要修改的键值数组
	 * @return $this
	 */
	public function set(array $data) {
		if (!key_exists('$set', $this->update_data))
			$this->update_data['$set'] = $data;
		else {
			$this->update_data['$set'] = array_merge($this->update_data['$set'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$unset 删除字段
	 * @param array $data 要删除的字段数组
	 * @return $this
	 */
	public function unSetFeild(array $data) {
		if (!key_exists('$unset', $this->update_data))
			$this->update_data['$unset'] = $data;
		else {
			$this->update_data['$unset'] = array_merge($this->update_data['$unset'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$rename 重命名字段
	 * @param array $data 要重命名的字段数组
	 * @return $this
	 */
	public function rename(array $data) {
		if (!key_exists('$rename', $this->update_data))
			$this->update_data['$rename'] = $data;
		else {
			$this->update_data['$rename'] = array_merge($this->update_data['$rename'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$setOnInsert 更新/插入键值
	 * @param array $data 要更新/插入键值数组
	 * @return $this
	 */
	public function setOnInsert(array $data) {
		if (!key_exists('$setOnInsert', $this->update_data))
			$this->update_data['$setOnInsert'] = $data;
		else {
			$this->update_data['$setOnInsert'] = array_merge($this->update_data['$setOnInsert'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$push 向指定字段中添加值
	 * @param array $data 要更添加的键值数组
	 * @return $this
	 */
	public function push(array $data) {
		if (!key_exists('$push', $this->update_data))
			$this->update_data['$push'] = $data;
		else {
			$this->update_data['$push'] = array_merge($this->update_data['$push'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$push 和 $each 向指定字段中添加多个值
	 * @param $field 指定的字段
	 * @param array $data 更新匹配记录的对象
	 * @return $this
	 */
	public function each($field, array $data) {
		$push_data = array($field => array('$each' => $data));
		return $this->push($push_data);
	}

	/**
	 * 更新数据 - 使用$addToSet 将数据添加到数组中
	 * @param array $data 更新匹配记录的对象
	 * @return $this
	 */
	public function addToSet(array $data) {
		if (!key_exists('$addToSet', $this->update_data))
			$this->update_data['$addToSet'] = $data;
		else {
			$this->update_data['$addToSet'] = array_merge($this->update_data['$addToSet'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$addToSet 和 $each 向指定字段中添加多个值
	 * @param $field 更新匹配记录的对象
	 * @param array $data 更新匹配记录的对象
	 * @return $this
	 */
	public function eachAddToSet($field, array $data) {
		$push_data = array($field => array('$each' => $data));
		return $this->addToSet($push_data);
	}

	/**
	 * 更新数据 - 使用$pop从指定字段中删除第一个或最后一个元素
	 * @param  array $data 更新匹配记录的对象
	 * @return $this
	 * @example
	 *  1、$client->pop(array("feildname"=>1)) //删除最后一个元素;
	 *  2、$client->pop(array("feildname"=> -1)) //删除最后一个元素;
	 */
	public function pop(array $data) {
		if (!key_exists('$pop', $this->update_data))
			$this->update_data['$pop'] = $data;
		else {
			$this->update_data['$pop'] = array_merge($this->update_data['$pop'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$pull 删除所指定值
	 * @param  array $data 更新匹配记录的对象
	 * @return $this
	 * @example $client->pull(array("e-mail"=>"****@qq.com"))
	 */
	public function pull(array $data) {
		if (!key_exists('$pull', $this->update_data))
			$this->update_data['$pull'] = $data;
		else {
			$this->update_data['$pull'] = array_merge($this->update_data['$pull'], $data);
		}
		return $this;
	}

	/**
	 * 更新数据 - 使用$pull 删除所指定值
	 * @param  array $data 更新匹配记录的对象
	 * @return $this
	 * @example $client->pull(array("e-mail"=>array("****@qq.com",""****@gmail.com"")))
	 */
	public function pullAll(array $data) {
		if (!key_exists('$pullAll', $this->update_data))
			$this->update_data['$pullAll'] = $data;
		else {
			$this->update_data['$pullAll'] = array_merge($this->update_data['$pullAll'], $data);
		}
		return $this;
	}

	#endregion

	#region 删除数据

	/**
	 * 从集合中删除记录
	 * @param $collectionName 集合名称
	 * @param array $options [optional] 指定选项
	 * @return bool
	 */
	public function remove($collectionName, array $options = array("justOne" => FALSE, "fsync" => FALSE, "w" => 1, "j" => FALSE)) {
		//初始化
		$this->_init();
		$collection = $this->_selectCollection($collectionName);
		if ($collection) {
			return $collection->remove($this->where, $options);
		}
		return FALSE;
	}

	#endregion

	#region 查询数据

	/**
	 * 返回单个文档
	 * @param $collectionName 集合名称
	 * @return array
	 */
	public function findOne($collectionName) {
		//初始化
		$this->_init();
		$collection = $this->_selectCollection($collectionName);
		if ($collection) {
			$where = $this->where;
			$this->where = array();
			$detail = $collection->findOne($where, $this->fields);
			return $detail;
		}
		return array();
	}

	/**
	 * 返回所有文档
	 * @param $collectionName 集合名称
	 * @return array
	 */
	public function find($collectionName) {
		//初始化
		$this->_init();
		$collection = $this->_selectCollection($collectionName);
		$docs = array();
		if ($collection) {
			$where = $this->where;
			$this->where = array();
			$cursor = $collection->find($where, $this->fields);

			// 设置排序
			if (isset($this->sort) && !empty($this->sort)) {
				$cursor->sort($this->sort);
			}
			// 限制
			if (isset($this->limit) && intval($this->limit)) {
				$cursor->limit($this->limit);
			}
			// 忽略数据项
			if (isset($this->skip) && intval($this->skip)) {
				$cursor->skip($this->skip);
			}
			// 忽略数据项
			if (isset($this->hint) && intval($this->hint)) {
				$cursor->hint($this->hint);
			}

			while ($doc = $cursor->getNext()) {
				$docs[] = $doc;
			}
		}
		return $docs;
	}

	/**
	 * 返回集合中的文档数量
	 * @param $collectionName 集合名称
	 * @param bool $all 是否返回全部
	 * @return int
	 */
	public function count($collectionName, $all = FALSE) {
		//初始化
		$this->_init();
		$collection = $this->_selectCollection($collectionName);
		if ($collection) {
			$where = $this->where;
			$this->where = array();
			$cursor = $collection->find($where, $this->fields);
			if ($all) {
				// 限制
				if (isset($this->limit) && intval($this->limit)) {
					$cursor->limit($this->limit);
				}
				// 忽略数据项
				if (isset($this->skip) && intval($this->skip)) {
					$cursor->skip($this->skip);
				}
			}
			return $cursor->count($all);
		}
		return 0;
	}

	#endregion

	#region MongoDBRef 相关

	/**
	 * 创建MongoDBRef链接
	 * @param $collectionName 集合名称
	 * @param $id 用于链接的文档_id
	 * @param $dbName 数据库名称
	 * @return array
	 */
	public function createRef($collectionName, $id, $dbName = null) {
		//初始化
		$this->_init();
		if (!$dbName) $dbName = $this->db->__toString();
		return MongoDBRef::create($collectionName, $id, $dbName);
	}

	/**
	 * 创建MongoDBRef链接
	 * @param $ref MongoDBRef链接对象
	 * @param $dbName 数据库名称
	 * @return array
	 */
	public function getRefData($ref, $dbName = null) {
		//初始化
		$this->_init();
		if ($dbName) $db = $this->client->selectDB($dbName);
		else $db = $this->db;
		return MongoDBRef::get($db, $ref);
	}

	/**
	 * 判断是否是链接对象
	 * @param $ref MongoDBRef链接对象
	 */
	public static function isRef($ref) {
		return MongoDBRef::isRef($ref);
	}

	#endregion

	#region 私有函数

	/**
	 * 初始化
	 */
	private function _init() {
		static $client;
		static $db;
		if (!isset($client) || !$client) {
			$client = new MongoClient("{$this->config->host}:{$this->config->port}", $this->config->options->toArray());
		}
		$this->client = $client;
		if (isset($db)) {
			$db = $this->client->selectDB($this->dbName);
		}
		$this->db = $db;
	}

	/**
	 * 获取集合
	 * @param $collectionName 集合名称
	 * @return \MongoCollection 集合对象
	 */
	private function _selectCollection($collectionName) {
		return $this->db->selectCollection($collectionName);
	}

	#endregion
}